﻿using System.Collections.Generic;
using GameLibrary;
using Microsoft.Xna.Framework;
using Rescue_of_the_Dwarvenzombies.SceneLayers;
using Rescue_of_the_Dwarvenzombies.SceneObjects;

namespace Rescue_of_the_Dwarvenzombies.Scene
{
    /// <summary>
    /// Responsible for game physics, object movement and collision detection
    /// </summary>
    internal class PhysicsManager : IPhysicsManager
    {
        /// <summary>
        /// Used to get the services (SceneManager) from game
        /// </summary>
        private Game _game;

        /// <summary>
        /// Indicates wether a character has collided with a obstacle
        /// or with another character
        /// </summary>
        private bool _collisionOccurred;

        /// <summary>
        /// Public constructor
        /// </summary>
        /// <param name="game">Reference to the game</param>
        public PhysicsManager(Game game)
        {
            _game = game;
            game.Services.AddService(typeof(IPhysicsManager), this);
        }

        /// <summary>
        /// Updates all game object according to their movements and checking collision between them
        /// </summary>
        /// <param name="gameTime">The game time.</param>
        public void Update(GameTime gameTime)
        {
            ISceneLayer rootLayer = ((ISceneManager)_game.Services.GetService(typeof(ISceneManager))).GetRootLayer();
            Vector2 newPosition, wantedMove;

            for (ISceneLayer layer = rootLayer.Child; layer.LayerName != Constant.Layer.Item; layer = layer.Child)
            {
                //Make a copy of the original list in order to be able to change the set
                List<ISceneObject> copy = new List<ISceneObject>(layer.GetSceneObjects());

                foreach (ISceneObject sceneObject in copy)
                {
                    newPosition = sceneObject.Position;
                    wantedMove = sceneObject.WantedMove * (float)gameTime.ElapsedGameTime.TotalSeconds;

                    //Check if the object is not out of bounds
                    if (layer.LayerName == Constant.Layer.Weapon && IsObjectOutOfBounds(sceneObject))
                    {
                        layer.RemoveSceneObject(sceneObject);
                        return;
                    }

                    CheckCollision(layer, sceneObject);

                    //If no collision detected, the object is allowed to move
                    if (!_collisionOccurred)
                    {
                        newPosition += wantedMove;
                        sceneObject.LastPosition = sceneObject.Position; //store last position for backtracking
                        sceneObject.Position = newPosition;
                    }
                    //If collision, move object to it's last position
                    else
                    {
                        sceneObject.Position = sceneObject.LastPosition;
                    }

                    //Update the bounding box with the new position
                    sceneObject.BoundingBox = new BoundingBox(new Vector3(sceneObject.Position.X, sceneObject.Position.Y, 0),
                            new Vector3(sceneObject.Position.X + sceneObject.RectangleWidth, sceneObject.Position.Y + sceneObject.RectangleHeight, 0));

                    //Reset the collision indicator
                    _collisionOccurred = false;
                }
            }
        }

        /// <summary>
        /// Checks for collisions.
        /// </summary>
        /// <param name="parent">The parent layer.</param>
        /// <param name="currentSceneObject">The current scene object to check.</param>
        private void CheckCollision(ISceneLayer parent, ISceneObject currentSceneObject)
        {
            if (parent.LayerName == Constant.Layer.Player)
            {
                CheckCollisionInParentLayer(parent, currentSceneObject);
            }

            if (!_collisionOccurred)
            {
                CheckCollisionInChildLayers(parent, currentSceneObject);
                //TESTING TESTING: Will apply if we get time to improve AI.
                //CheckCollisionInSameLayer(parent, currentSceneObject);
            }
        }

        //Support Method for improved AI, not yet implemented.
        private void CheckCollisionInSameLayer(ISceneLayer parent, ISceneObject currentSceneObject)
        {
            if (parent is EnemyLayer)
            {
                ISceneLayer enemyLayer = parent;
                List<ISceneObject> enemies = ((EnemyLayer)enemyLayer).GetSceneObjects();
                for (int i = 0; i < enemies.Count; i++)
                {
                    if (currentSceneObject.Position != enemies[i].Position)
                        if (currentSceneObject.BoundingBox.Intersects(enemies[i].BoundingBox))
                            _collisionOccurred = true;
                }
            }
        }

        /// <summary>
        /// Checks the collision within parent layer.
        /// </summary>
        /// <param name="parent">The parent layer.</param>
        /// <param name="currentSceneObject">The current scene object.</param>
        private void CheckCollisionInParentLayer(ISceneLayer parent, ISceneObject currentSceneObject)
        {
            //Check collision within it's own layer
            List<ISceneObject> copy = new List<ISceneObject>(parent.GetSceneObjects());
            foreach (ISceneObject sceneObject in copy)
            {
                //Check possible collision (can't collide with itself)
                if (currentSceneObject != sceneObject &&
                    currentSceneObject.BoundingBox.Intersects(sceneObject.BoundingBox))
                {
                    //Check pixel collision
                    if (IntersectPixels(currentSceneObject, sceneObject))
                    {
                        _collisionOccurred = true;
                    }
                }
            }
        }

        /// <summary>
        /// Checks for possible collisions in all children layers.
        /// </summary>
        /// <param name="parent">The parent layer.</param>
        /// <param name="currentSceneObject">The current scene object.</param>
        private void CheckCollisionInChildLayers(ISceneLayer parent, ISceneObject currentSceneObject)
        {
            Constant.Layer childLayerName;
            ISceneManager sceneManager = (ISceneManager)_game.Services.GetService(typeof(ISceneManager));

            //Check all child layers until last Layer is found..
            for (ISceneLayer child = parent.Child; child != null; child = child.Child)
            {
                childLayerName = child.LayerName;

                //Make a copy of the original list in order to be able to change the set
                List<ISceneObject> copy = new List<ISceneObject>(child.GetSceneObjects());

                foreach (ISceneObject childObject in copy)
                {
                    //Check possible collision (Players can't collide with player weapons)
                    if (currentSceneObject.BoundingBox.Intersects(childObject.BoundingBox) && childLayerName != Constant.Layer.Weapon)
                    {
                        //Only need to check pixel collision on movable objects
                        if (typeof(IStatic).IsAssignableFrom(childObject.GetType()) || IntersectPixels(currentSceneObject, childObject))
                        {
                            //Delegate collision consequence handling to the Scene Manager
                            sceneManager.HandleCollision(currentSceneObject, childObject, parent, child);

                            if (childLayerName == Constant.Layer.Obstacle)
                                _collisionOccurred = true;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Determines if there is overlap of the non-transparent pixels
        /// between two sprites.
        /// </summary>
        /// <param name="currentSceneObject">The Scene Object</param>
        /// <param name="dataA">Pixel data of the first sprite</param>
        /// <param name="childObject">The child Scene Object</param>
        /// <param name="dataB">Pixel data of the second sprite</param>
        /// <returns>True if non-transparent pixels overlap; false otherwise</returns>
        private bool IntersectPixels(ISceneObject currentSceneObject, ISceneObject childObject)
        {
            //The pixel data from the axe is sometimes null...
            if (currentSceneObject.PixelData == null || childObject.PixelData == null)
                return false;

            Rectangle rectangleA = new Rectangle((int)currentSceneObject.BoundingBox.Min.X,
                                                 (int)currentSceneObject.BoundingBox.Min.Y, currentSceneObject.RectangleWidth,
                                                 currentSceneObject.RectangleHeight);

            Rectangle rectangleB = new Rectangle((int)childObject.BoundingBox.Min.X,
                                                 (int)childObject.BoundingBox.Min.Y, childObject.RectangleWidth,
                                                 childObject.RectangleHeight);

            Rectangle intersectionRectangle = Rectangle.Intersect(rectangleA, rectangleB);

            // Check every point within the intersection bounds
            for (int y = intersectionRectangle.Top; y < intersectionRectangle.Bottom; y++)
            {
                for (int x = intersectionRectangle.Left; x < intersectionRectangle.Right; x++)
                {
                    // Get the color of both pixels at this point
                    Color colorA = currentSceneObject.PixelData[(x - rectangleA.Left) +
                                                                (y - rectangleA.Top) * rectangleA.Width];
                    Color colorB = childObject.PixelData[(x - rectangleB.Left) +
                                                         (y - rectangleB.Top) * rectangleB.Width];

                    // If both pixels are not completely transparent,
                    if (colorA.A != 0 && colorB.A != 0)
                    {
                        // then an intersection has been found
                        return true;
                    }
                }
            }
            // No intersection found
            return false;
        }

        /// <summary>
        /// Checks if the object is out of bounds to improve performance.
        /// </summary>
        /// <param name="sceneObject">The Scene Object (Weapon) to check if out of bounds</param>
        /// <returns>Returns true if the object is out of bounds, false otherwise.</returns>
        public bool IsObjectOutOfBounds(ISceneObject sceneObject)
        {
            //Get camera
            ICamera camera = (ICamera)_game.Services.GetService(typeof(ICamera));

            //Check if it's inside the camera's bounding box
            return !camera.BoundingBox.Intersects(sceneObject.BoundingBox);
        }

        /// <summary>
        /// Checks if an enemy is closer than a minimum distance.
        /// If so said enemy will start traversing in the opposite direction
        /// of the nearest enemy
        /// </summary>
        /// <param name="enemyLayer">The enemy layer.</param>
        /// <param name="objectLayer">The object layer.</param>
        /// <param name="move">The move.</param>
        /// <param name="currentEnemyPosition">The current enemy position.</param>
        /// <returns></returns>
        public bool PossibleMove(ISceneLayer enemyLayer, ISceneLayer objectLayer, ref Vector2 move, Vector2 currentEnemyPosition)
        {
            List<ISceneObject> enemies = enemyLayer.GetSceneObjects();
            //List<ISceneObject> objects = objectLayer.GetSceneObjects();
            float minDistance = 60; //Minimum (pixel) distance between objects in the world (except for players)

            for (int i = 0; i < enemies.Count; i++)
            {
                float distance = Vector2.Distance(enemies[i].Position, currentEnemyPosition);

                if (minDistance > distance && distance != 0)
                {
                    minDistance = distance;
                    move = currentEnemyPosition - enemies[i].Position;
                    move.Normalize();
                }
            }
            
            //AI for objects not yet implemented
            
            if (move.X == 0 && move.Y == 0)
                return true;
            else
                return false;
        }
    }
}